// <copyright file="Plane.cs" company="Community Combat Simulator">
//    Community Combat Simulator: a free combat flight simulator.
//    Copyright (C) 2008 Aaron Schinkowitch
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see http://www.gnu.org/licenses/.
// </copyright>
namespace CCSim.Vecmath
{
    using System;
    using CCSim.Util;
    
    /// <summary>
    /// Defines the relative position to a plane.
    /// </summary>
    public enum RelativePosition 
    {
        /// <summary>
        /// Above the plane.
        /// </summary>
        Above,
        
        /// <summary>
        /// On the plane.
        /// </summary>
        Coplanar, 
        
        /// <summary>
        /// Below the plane.
        /// </summary>
        Below
    }

    /// <summary>
    /// A Plane is the set of all points in R3 that satisfy the equation: ax + by + cz + d = 0.
    /// </summary>
    public class Plane
    {
        /// <summary>
        /// The margin within which a point must be to be considered on the plane.
        /// </summary>
        internal const double CoplanarMargin = 1E-5;
        
        /// <summary>
        /// Parameter in the equation of a plane: ax + by + cz + d = 0.
        /// </summary>
        private double a, b, c, d;
        
        /// <summary>
        /// Initializes a new instance of the Plane class defined by 3 vertices. The vertices must 
        /// not be colinear in order to define a plane.
        /// </summary>
        /// <param name="v1">The first vertex.</param>
        /// <param name="v2">The second vertex.</param>
        /// <param name="v3">The third vertex.</param>
        /// <exception cref="ArgumentOutOfRangeException">The vertices do not define a plane 
        /// (are colinear).</exception>
        public Plane(Vector v1, Vector v2, Vector v3)
        {
            if (v1 == null) 
            {
                throw new ArgumentNullException("v1");
            }
            
            if (v2 == null)
            {
                throw new ArgumentNullException("v2");
            }
            
            if (v3 == null)
            {
                throw new ArgumentNullException("v3");
            }
            
            // A = y1 (z2 - z3) + y2 (z3 - z1) + y3 (z1 - z2)
            this.A = (v1.Y * (v2.Z - v3.Z)) + (v2.Y * (v3.Z - v1.Z)) + (v3.Y * (v1.Z - v2.Z));
            
            // B = z1 (x2 - x3) + z2 (x3 - x1) + z3 (x1 - x2) 
            this.B = (v1.Z * (v2.X - v3.X)) + (v2.Z * (v3.X - v1.X)) + (v3.Z * (v1.X - v2.X));
            
            // C = x1 (y2 - y3) + x2 (y3 - y1) + x3 (y1 - y2)
            this.C = (v1.X * (v2.Y - v3.Y)) + (v2.X * (v3.Y - v1.Y)) + (v3.X * (v1.Y - v2.Y));
            
            // -D = x1 (y2 z3 - y3 z2) + x2 (y3 z1 - y1 z3) + x3 (y1 z2 - y2 z1)
            this.D = -((v1.X * ((v2.Y * v3.Z) - (v3.Y * v2.Z)))
                       + (v2.X * ((v3.Y * v1.Z) - (v1.Y * v3.Z))) 
                       + (v3.X * ((v1.Y * v2.Z) - (v2.Y * v1.Z))));
            
            Equality equality = new Equality();
            
            if (equality.RoughlyEqual(this.A, 0.0, CoplanarMargin) 
                && equality.RoughlyEqual(this.B, 0.0, CoplanarMargin)
                && equality.RoughlyEqual(this.C, 0.0, CoplanarMargin))
            {
                throw new ArgumentException("Vectors are colinear.");
            }
        }
        
        /// <summary>
        /// Initializes a new instance of the Plane class defined by a point on the plane and the 
        /// plane's normal.
        /// </summary>
        /// <param name="vertex">A point on the plane.</param>
        /// <param name="normal">The normal of the plane. Must be non-zero.</param>
        /// <exception cref="ArgumentOutOfRangeException">Zero normal.</exception>
        public Plane(Vector vertex, Vector normal)
        {
            if (vertex == null)
            {
                throw new ArgumentNullException("vertex");
            }
            
            if (normal == null)
            {
                throw new ArgumentNullException("normal");
            }
            
            if (normal.IsZero())
            {
                throw new ArgumentOutOfRangeException("normal", "normal cannot be zero vector");
            }
            
            this.A = normal.X;
            this.B = normal.Y;
            this.C = normal.Z;
            this.D = -vertex.Dot(normal);
        }

        /// <summary>
        /// Gets or sets a paramter in the equation of a plane: ax + by + cz + d = 0.
        /// </summary>
        internal double A
        {
            get { return this.a; }
            set { this.a = value; }
        }
        
        /// <summary>
        /// Gets or sets a paramter in the equation of a plane: ax + by + cz + d = 0.
        /// </summary>
        internal double B 
        {
            get { return this.b; }
            set { this.b = value; }
        }
        
        /// <summary>
        /// Gets or sets a paramter in the equation of a plane: ax + by + cz + d = 0.
        /// </summary>
        internal double C 
        {
            get { return this.c; }
            set { this.c = value; }
        }
        
        /// <summary>
        /// Gets or sets a paramter in the equation of a plane: ax + by + cz + d = 0.
        /// </summary>
        internal double D 
        {
            get { return this.d; }
            set { this.d = value; }
        }
        
        /// <summary>
        /// Determine whether a given point is above, below or on the plane.
        /// </summary>
        /// <param name="point">The point in R3.</param>
        /// <returns>The relative position to the plane.</returns>
        public RelativePosition GetRelativePosition(Vector point)
        {
            if (point == null)
            {
                throw new ArgumentNullException("point");
            }
            
            double relativeValue 
                = (point.X * this.A) + (point.Y * this.B) + (point.Z * this.C) + this.D;
            
            if (relativeValue > CoplanarMargin)
            {
                return RelativePosition.Above;
            }
            else if (relativeValue < -CoplanarMargin)
            {
                return RelativePosition.Below;
            }
            else 
            {
                return RelativePosition.Coplanar;
            }
        }
    }
}
